package at.ac.tuwien.swa.sparrow.server.security.web

import at.ac.tuwien.swa.sparrow.common.Account
import at.ac.tuwien.swa.sparrow.server.component.UserManager
import at.ac.tuwien.swa.sparrow.server.persistence.{AccountRepository, PeerRepository}
import at.ac.tuwien.swa.sparrow.server.security.SecurityContext
import com.typesafe.scalalogging.log4j.Logging
import java.util.regex.Pattern
import javax.inject.Inject
import javax.servlet._
import javax.servlet.http._
import scala.Some

object SecurityFilter extends Filter with Logging {

	type Exchange = (HttpServletRequest, HttpServletResponse)
	type AccessDecisionHandler = Exchange => Boolean
	type Role = AnyRef with Serializable

	val SESSION_ROLE_OBJECT = "__SWA_role_object"
	@Inject var accountRepo: AccountRepository = _
	@Inject var peerRepo: PeerRepository = _

	private var loginUrlPattern: Pattern = null
	private val accessDecisionHandlers = Array[AccessDecisionHandler](
		ex => {
			if ((ex._1 getHeader "peerId") == null) false
			else peerRepo findOne (_._2.peerId.toString == (ex._1 getHeader "peerId")) match {
				case None => false
				case Some(p) => SecurityContext.setRole(p)
					true
			}
		},
		ex => {
			val username = ex._1 getHeader "username"
			val password = ex._1 getHeader "password"
			username != null && password != null &&
				(accountRepo findOne (p => p._2.username == username && (p._2 passwordMatches password))).isDefined
		}
	)

	def setAuthenticated(session: HttpSession, roleObject: Role) {
		if (session != null) {
			session.setAttribute(SESSION_ROLE_OBJECT, roleObject)
			SecurityContext.setRole(roleObject)
			logger debug s"Authenticated session ${session.getId}. Role object is $roleObject"
		} else {
			SecurityContext.setRole(null)
		}
	}

	@inline def role(implicit session: HttpSession) =
		(session getAttribute SecurityFilter.SESSION_ROLE_OBJECT).asInstanceOf[Role]

	@inline def isAuthenticated(implicit session: HttpSession) = role(session) != null

	def init(filterConfig: FilterConfig) {
		val loginUrl = filterConfig getInitParameter "LOGIN_URL"
		if (loginUrl == null || loginUrl.trim.isEmpty) {
			logger error "No login url (configuration parameter LOGIN_URL) specified."
		} else {
			loginUrlPattern = Pattern compile loginUrl
			logger info s"${filterConfig.getFilterName} configured. Using login url $loginUrl"
		}
	}

	def doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
		val request = servletRequest.asInstanceOf[HttpServletRequest]
		val response = servletResponse.asInstanceOf[HttpServletResponse]
		implicit val session = request getSession true

		if (role != null) {
			logger debug s"Session ${session.getId} is valid"
			SecurityContext.setRole(role)
			performFilterChain(request, response, filterChain)
		} else if (loginUrlPattern != null && (loginUrlPattern matcher request.getRequestURI).matches) {
			logger debug s"Accessing login url ${request.getRequestURI}"
		}
		accessDecisionHandlers find (_(request, response)) match {
			case Some(r) => performFilterChain(request, response, filterChain)
			case None => logger.debug(s"Access denied for session ${request.getSession(true).getId} on ${request.getRequestURI}")
				response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
		}
	}

	private def performFilterChain(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
		val authenticated = isAuthenticated(request.getSession)
		try {
			filterChain.doFilter(request, response)
		} finally {
			if (!authenticated) {
				// If the user has not been authenticated when the request was received but is now, update the SecurityContext.
				SecurityContext.getRole match {
					case a: Account => setAuthenticated(request.getSession, a)
					case _ =>
				}
			}
			SecurityContext.setRole(null)
		}
	}

	override def destroy() {
	}
}

class HttpAuthenticationListener @Inject()(userManager: UserManager) extends HttpSessionListener {
	def sessionCreated(se: HttpSessionEvent) {}

	def sessionDestroyed(se: HttpSessionEvent) =
		userManager.tokens find (_._2 map (_.sid) contains se.getSession.getId) match {
			case Some(t) => userManager.logout(t._1)
			case None =>
		}
}
